Skip to content

CAMEL-25823: camel-core - Start Split and Multicast output producers when the route starts - #28147

Open
gnodet wants to merge 22 commits into
apache:mainfrom
gnodet:camel-25823-multicast-split-deferred-start
Open

gnodet wants to merge 22 commits into
apache:mainfrom
gnodet:camel-25823-multicast-split-deferred-start

Conversation

@gnodet

@gnodet gnodet commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Defer error handler startup in MulticastProcessor from doInit() to doStart() to fix compatibility with SupervisingRouteController when a producer cannot connect at context startup.

Splitter extends MulticastProcessor and is fixed by this change as well.

Root Cause

MulticastProcessor.doInit() calls wrapInErrorHandler() for each child processor, which eagerly invoked ServiceHelper.startService(answer) on the wrapped error handler. Starting the error handler starts the wrapped SendProcessor, which in turn starts the underlying Producer (via camelContext.addService(producer, true, true)).

This producer startup happens:

  • During route initialization, before CamelContext is fully started
  • Outside the SupervisingRouteController retry scope

A producer that cannot connect yet (e.g. broker is temporarily down) propagates the exception directly to context.start(), failing the entire context — even though the route was marked for supervision.

Fix

Three changes in MulticastProcessor:

  1. In wrapInErrorHandler(): replaced the unconditional ServiceHelper.startService(answer) with a conditional start gated on isRunAllowed(). isRunAllowed() returns true in STARTING, STARTED, SUSPENDING, and SUSPENDED states, covering both wrappers built at runtime (e.g. by RecipientListProcessor) and those built during route startup. During doInit() the processor is in INITIALIZING state, so isRunAllowed() is false and the built error handler is cached in errorHandlers but not started. The wrapper is now started before being published to the cache, avoiding a race where a concurrent exchange could take the cache-hit path and use an unstarted wrapper.

  2. In doStart(): added ServiceHelper.startService(errorHandlers.values()) to start any pre-built error handlers that were deferred from doInit(). This runs under the route lifecycle managed by SupervisingRouteController, so failures are caught and the route is scheduled for retry with back-off. ServiceHelper.start() is idempotent (no-op if already started), so there is no double-start risk when new error handlers are built at runtime and doStart() iterates over errorHandlers.values().

  3. In doStop() and doShutdown(): corrected ServiceHelper.stopService(errorHandlers) / ServiceHelper.shutdownService(errorHandlers) to pass errorHandlers.values() instead of the raw Map. The previous code silently did nothing because Map does not implement Iterable; now the cached per-branch error handlers are correctly stopped and shut down with the route.

One additional fix in RedeliveryErrorHandler.SimpleTask.runNotAllowed():

  1. Null-logger guard and single-WARN guarantee: when an exchange already carries a RouteStoppingException and its error handler is stopped at route shutdown, runNotAllowed() now guards logger != null before logging (matching the guard in logFailedDelivery, since LegacyDefaultErrorHandlerReifier passes a null logger). It also calls exchange.getExchangeExtension().setInterrupted(true) after logging the cut-off WARN, so nested stopped error handlers (e.g. a split inside a multicast) do not each emit their own WARN for the same exchange. When the WARN is logged and the exchange is marked as interrupted, any caller route that passed the same exchange into the stopped route via direct also skips its onException, redelivery, and dead letter channel — the same behavior as DirectProducer (CAMEL-25502).

    Behavioral note: setInterrupted(true) is the same mechanism DirectProducer uses (CAMEL-25502) when a route is stopped while an exchange is waiting for a consumer. Setting it causes the error handler to skip redelivery, onException handlers, and the dead letter channel for that exchange — which is the correct behavior for a forced route-stop cut-off. An exchange stopped mid-route should not be redelivered or sent to the DLC; the RouteStoppingException signals a clean cut-off, not a failure. This applies to any stopped error handler — not just Split or Multicast wrappers — including filter, choice, doTry, loop, and other nested EIPs. See also the upgrade guide note below.

Upgrade Guide

An upgrade guide entry has been added to camel-4x-upgrade-guide-4_24.adoc documenting the three behavioral changes for users who may have been relying on the previous (incorrect) lifecycle behavior.

Tests

New test class DefaultSupervisingRouteControllerMulticastSplitTest with two test methods:

  • testSplitWithFlakyProducer() — a split().to("flaky:target") route where the producer fails its first 2 doStart() calls. Asserts context.start() does not throw and the route eventually reaches Started after the supervising controller retries.
  • testMulticastWithFlakyProducer() — same scenario with multicast().to("flaky:target").

New test class RouteStopCutOffLogNestedTest with one test method:

  • nestedEipCutOffIsOneWarnLine() — a multicast() wrapping a split() where an inflight exchange waits on direct:shipment. When the route is stopped, the test waits for the route to fully stop and all inflight exchanges to drain, then asserts exactly one WARN cut-off line, verifying that setInterrupted(true) prevents nested stopped error handlers from each logging their own WARN for the same exchange.

New test class RouteStopCutOffCallerRouteTest with one test method:

  • callerRouteDlcNotCalledOnNestedEipCutOff() — route A (with a dead letter channel) sends via direct to route B, which is a multicast() wrapping a split(). Route B is force-stopped while an inflight exchange is blocked inside the nested EIP. The test asserts that route A's dead letter channel is NOT called: the setInterrupted(true) marker set on the exchange by route B's error handler propagates back to route A, which then skips redelivery, onException, and the DLC.

Fixes #CAMEL-25823


Hermes Agent (Claude Sonnet 4.6) on behalf of Guillaume Nodet

@github-actions github-actions Bot added the core label Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • core/camel-core-processor
  • core/camel-core
  • docs

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.

🔁 2 tests passed only after a retry on JDK 21 (2 retried attempts)

Recovered flaky tests on JDK 21 (2)
Module Test Failed attempts First failure
core/camel-core FileConsumerIdempotentKeyNameAndSizeTest.testIdempotentDiffSize 1 mock://result Body of message: 0. Expected: <Hello World> but was: <>
core/camel-core FileConsumerIncludeNameTest.testIncludePreAndPostfixes 1 mock://result Message with body Reports1 was expected but not found in [, Reports3, Reports2]

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 567 of 707 tested, 26 compile-only — current: 568 all tested

Maveniverse Scalpel detected 567 affected modules (current approach: 568).

Skip-tests mode would test 567 modules (3 direct + 565 downstream), skip tests for 26 (generated code, meta-modules)

Modules only in current approach (1)
  • camel-test-infra-core
Modules Scalpel would test (567)
  • archetypes ← downstream of org.apache.camel:camel-test-junit6
  • camel-a2a ← downstream of org.apache.camel:camel-core-model
  • camel-activemq ← downstream of org.apache.camel:camel-core-model
  • camel-activemq6 ← downstream of org.apache.camel:camel-core-model
  • camel-ai-observability ← downstream of org.apache.camel:camel-core-model
  • camel-ai-observability-api ← downstream of org.apache.camel:camel-core-model
  • camel-ai-parent ← downstream of org.apache.camel:camel-core-model
  • camel-ai-resource ← downstream of org.apache.camel:camel-core-model
  • camel-ai-tool ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-common ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-eventbridge ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-fc ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-kms ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-mns ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-oss ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-ots ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-parent ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-sls ← downstream of org.apache.camel:camel-core-model
  • camel-alibaba-sms ← downstream of org.apache.camel:camel-core-model
  • camel-amqp ← downstream of org.apache.camel:camel-core-model
  • camel-api-component-maven-plugin ← downstream of org.apache.camel:camel-test-junit6
  • camel-arangodb ← downstream of org.apache.camel:camel-core-model
  • camel-archetype-api-component ← downstream of org.apache.camel:camel-test-junit6
  • camel-archetype-component ← downstream of org.apache.camel:camel-test-junit6
  • camel-archetype-dataformat ← downstream of org.apache.camel:camel-test-junit6
  • camel-archetype-java ← downstream of org.apache.camel:camel-test-junit6
  • camel-archetype-main ← downstream of org.apache.camel:camel-test-junit6
  • camel-as2 ← downstream of org.apache.camel:camel-core-model
  • camel-as2-api ← downstream of org.apache.camel:camel-core-model
  • camel-as2-parent ← downstream of org.apache.camel:camel-core-model
  • camel-asn1 ← downstream of org.apache.camel:camel-core-model
  • camel-asterisk ← downstream of org.apache.camel:camel-core-model
  • camel-atmosphere-websocket ← downstream of org.apache.camel:camel-core-model
  • camel-atom ← downstream of org.apache.camel:camel-core-model
  • camel-attachments ← downstream of org.apache.camel:camel-core-model
  • camel-avro ← downstream of org.apache.camel:camel-core-model
  • camel-avro-rpc ← downstream of org.apache.camel:camel-core-model
  • camel-avro-rpc-jetty ← downstream of org.apache.camel:camel-core-model
  • camel-avro-rpc-parent ← downstream of org.apache.camel:camel-core-model
  • camel-avro-rpc-spi ← downstream of org.apache.camel:camel-core-model
  • camel-aws-bedrock ← downstream of org.apache.camel:camel-core-model
  • camel-aws-cloudtrail ← downstream of org.apache.camel:camel-core-model
  • camel-aws-common ← downstream of org.apache.camel:camel-core-model
  • camel-aws-config ← downstream of org.apache.camel:camel-core-model
  • camel-aws-parameter-store ← downstream of org.apache.camel:camel-core-model
  • camel-aws-parent ← downstream of org.apache.camel:camel-core-model
  • camel-aws-secrets-manager ← downstream of org.apache.camel:camel-core-model
  • camel-aws-security-hub ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-athena ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-comprehend ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-cw ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-ddb ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-ec2 ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-ecs ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-eks ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-eventbridge ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-iam ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-kinesis ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-kms ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-lambda ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-mq ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-msk ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-polly ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-redshift ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-rekognition ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-s3 ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-s3-vectors ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-ses ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-sns ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-sqs ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-step-functions ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-sts ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-textract ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-timestream ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-transcribe ← downstream of org.apache.camel:camel-core-model
  • camel-aws2-translate ← downstream of org.apache.camel:camel-core-model
  • camel-azure-common ← downstream of org.apache.camel:camel-core-model
  • camel-azure-cosmosdb ← downstream of org.apache.camel:camel-core-model
  • camel-azure-eventgrid ← downstream of org.apache.camel:camel-core-model
  • camel-azure-eventhubs ← downstream of org.apache.camel:camel-core-model
  • camel-azure-files ← downstream of org.apache.camel:camel-core-model
  • camel-azure-functions ← downstream of org.apache.camel:camel-core-model
  • camel-azure-key-vault ← downstream of org.apache.camel:camel-core-model
  • camel-azure-parent ← downstream of org.apache.camel:camel-core-model
  • camel-azure-schema-registry ← downstream of org.apache.camel:camel-core-model
  • camel-azure-servicebus ← downstream of org.apache.camel:camel-core-model
  • camel-azure-storage-blob ← downstream of org.apache.camel:camel-core-model
  • camel-azure-storage-datalake ← downstream of org.apache.camel:camel-core-model
  • camel-azure-storage-queue ← downstream of org.apache.camel:camel-core-model
  • camel-barcode ← downstream of org.apache.camel:camel-core-model
  • camel-base64 ← downstream of org.apache.camel:camel-core-model
  • camel-bean ← downstream of org.apache.camel:camel-core-model
  • camel-bean-validator ← downstream of org.apache.camel:camel-core-model
  • camel-beanio ← downstream of org.apache.camel:camel-core-model
  • camel-bindy ← downstream of org.apache.camel:camel-core-model
  • camel-bonita ← downstream of org.apache.camel:camel-core-model
  • camel-box ← downstream of org.apache.camel:camel-core-model
  • camel-box-api ← downstream of org.apache.camel:camel-core-model
  • camel-box-parent ← downstream of org.apache.camel:camel-core-model
  • camel-braintree ← downstream of org.apache.camel:camel-core-model
  • camel-browse ← downstream of org.apache.camel:camel-core-model
  • camel-caffeine ← downstream of org.apache.camel:camel-core-model
  • camel-camunda ← downstream of org.apache.camel:camel-core-model
  • camel-cassandraql ← downstream of org.apache.camel:camel-core-model
  • camel-cbor ← downstream of org.apache.camel:camel-core-model
  • camel-chatscript ← downstream of org.apache.camel:camel-core-model
  • camel-chunk ← downstream of org.apache.camel:camel-core-model
  • camel-cli-connector ← downstream of org.apache.camel:camel-test-junit6
  • camel-cli-debug ← downstream of org.apache.camel:camel-cli-connector
  • camel-clickhouse ← downstream of org.apache.camel:camel-core-model
  • camel-clickup ← downstream of org.apache.camel:camel-core-model
  • camel-cloudevents ← downstream of org.apache.camel:camel-core-model
  • camel-cluster ← downstream of org.apache.camel:camel-core-model
  • camel-cm-sms ← downstream of org.apache.camel:camel-core-model
  • camel-coap ← downstream of org.apache.camel:camel-core-model
  • camel-cometd ← downstream of org.apache.camel:camel-core-model
  • camel-console ← downstream of org.apache.camel:camel-management
  • camel-consul ← downstream of org.apache.camel:camel-core-model
  • camel-controlbus ← downstream of org.apache.camel:camel-core-model
  • camel-core ← core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java, core/camel-core/src/test/java/org/apache/camel/processor/errorhandler/RouteStopCutOffCallerRouteTest.java, core/camel-core/src/test/java/org/apache/camel/processor/errorhandler/RouteStopCutOffLogNestedTest.java
  • camel-core-all ← downstream of org.apache.camel:camel-core-processor
  • camel-core-engine ← downstream of org.apache.camel:camel-core-model
  • camel-core-languages ← downstream of org.apache.camel:camel-core-model
  • camel-core-model ← downstream of org.apache.camel:camel-core-processor
  • camel-core-processor ← core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java, core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
  • camel-core-reifier ← downstream of org.apache.camel:camel-core-processor
  • camel-core-xml ← downstream of org.apache.camel:camel-core-engine
  • camel-couchbase ← downstream of org.apache.camel:camel-core-model
  • camel-couchdb ← downstream of org.apache.camel:camel-core-processor
  • camel-cron ← downstream of org.apache.camel:camel-core-model
  • camel-crypto ← downstream of org.apache.camel:camel-core-model
  • camel-crypto-pgp ← downstream of org.apache.camel:camel-core-model
  • camel-csv ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-common ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-parent ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-rest ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-soap ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-spring-common ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-spring-rest ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-spring-soap ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-spring-transport ← downstream of org.apache.camel:camel-core-model
  • camel-cxf-transport ← downstream of org.apache.camel:camel-core-model
  • camel-cyberark-vault ← downstream of org.apache.camel:camel-core-model
  • camel-dapr ← downstream of org.apache.camel:camel-core-model
  • camel-dataformat ← downstream of org.apache.camel:camel-core-model
  • camel-dataset ← downstream of org.apache.camel:camel-core-model
  • camel-datasonnet ← downstream of org.apache.camel:camel-core-model
  • camel-dataweave ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-common ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-common-parent ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-db2 ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-maven-plugin ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-mongodb ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-mysql ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-oracle ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-parent ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-postgres ← downstream of org.apache.camel:camel-core-model
  • camel-debezium-sqlserver ← downstream of org.apache.camel:camel-core-model
  • camel-debug ← downstream of org.apache.camel:camel-core-model
  • camel-dfdl ← downstream of org.apache.camel:camel-core-model
  • camel-dhis2 ← downstream of org.apache.camel:camel-core-model
  • camel-dhis2-api ← downstream of org.apache.camel:camel-core-model
  • camel-dhis2-parent ← downstream of org.apache.camel:camel-core-model
  • camel-diagram ← downstream of org.apache.camel:camel-core-model
  • camel-direct ← downstream of org.apache.camel:camel-core-model
  • camel-disruptor ← downstream of org.apache.camel:camel-core-model
  • camel-djl ← downstream of org.apache.camel:camel-core-model
  • camel-dns ← downstream of org.apache.camel:camel-core-model
  • camel-docker ← downstream of org.apache.camel:camel-core-model
  • camel-docling ← downstream of org.apache.camel:camel-core-model
  • camel-drill ← downstream of org.apache.camel:camel-core-model
  • camel-dropbox ← downstream of org.apache.camel:camel-core-model
  • camel-dsl-modeline ← downstream of org.apache.camel:camel-test-junit6
  • camel-dsl-support ← downstream of org.apache.camel:camel-core-model
  • camel-duckdb ← downstream of org.apache.camel:camel-core-model
  • camel-dynamic-router ← downstream of org.apache.camel:camel-core-processor
  • camel-ehcache ← downstream of org.apache.camel:camel-core-model
  • camel-elasticsearch ← downstream of org.apache.camel:camel-core-model
  • camel-elasticsearch-rest-client ← downstream of org.apache.camel:camel-core-model
  • camel-event ← downstream of org.apache.camel:camel-core-model
  • camel-exec ← downstream of org.apache.camel:camel-core-model
  • camel-fastjson ← downstream of org.apache.camel:camel-core-model
  • camel-fhir ← downstream of org.apache.camel:camel-core-model
  • camel-fhir-api ← downstream of org.apache.camel:camel-core-model
  • camel-fhir-parent ← downstream of org.apache.camel:camel-core-model
  • camel-file ← downstream of org.apache.camel:camel-core-model
  • camel-file-watch ← downstream of org.apache.camel:camel-core-model
  • camel-flatpack ← downstream of org.apache.camel:camel-core-model
  • camel-flink ← downstream of org.apache.camel:camel-core-model
  • camel-flowable ← downstream of org.apache.camel:camel-core-model
  • camel-fop ← downstream of org.apache.camel:camel-core-model
  • camel-fory ← downstream of org.apache.camel:camel-core-model
  • camel-freemarker ← downstream of org.apache.camel:camel-core-model
  • camel-ftp ← downstream of org.apache.camel:camel-core-model
  • camel-ftp-common ← downstream of org.apache.camel:camel-core-model
  • camel-geocoder ← downstream of org.apache.camel:camel-core-model
  • camel-git ← downstream of org.apache.camel:camel-core-model
  • camel-github2 ← downstream of org.apache.camel:camel-core-model
  • camel-google-bigquery ← downstream of org.apache.camel:camel-core-model
  • camel-google-calendar ← downstream of org.apache.camel:camel-core-model
  • camel-google-common ← downstream of org.apache.camel:camel-core-model
  • camel-google-drive ← downstream of org.apache.camel:camel-core-model
  • camel-google-firestore ← downstream of org.apache.camel:camel-core-model
  • camel-google-functions ← downstream of org.apache.camel:camel-core-model
  • camel-google-mail ← downstream of org.apache.camel:camel-core-model
  • camel-google-parent ← downstream of org.apache.camel:camel-core-model
  • camel-google-pubsub ← downstream of org.apache.camel:camel-core-model
  • camel-google-secret-manager ← downstream of org.apache.camel:camel-core-model
  • camel-google-sheets ← downstream of org.apache.camel:camel-core-model
  • camel-google-speech-to-text ← downstream of org.apache.camel:camel-core-model
  • camel-google-storage ← downstream of org.apache.camel:camel-core-model
  • camel-google-text-to-speech ← downstream of org.apache.camel:camel-core-model
  • camel-google-vertexai ← downstream of org.apache.camel:camel-core-model
  • camel-google-vision ← downstream of org.apache.camel:camel-core-model
  • camel-graphql ← downstream of org.apache.camel:camel-core-model
  • camel-grok ← downstream of org.apache.camel:camel-core-model
  • camel-groovy ← downstream of org.apache.camel:camel-core-model
  • camel-grpc ← downstream of org.apache.camel:camel-core-model
  • camel-gson ← downstream of org.apache.camel:camel-core-model
  • camel-hashicorp-vault ← downstream of org.apache.camel:camel-core-model
  • camel-hazelcast ← downstream of org.apache.camel:camel-core-model
  • camel-hivemq ← downstream of org.apache.camel:camel-core-model
  • camel-hl7 ← downstream of org.apache.camel:camel-core-model
  • camel-http ← downstream of org.apache.camel:camel-core-model
  • camel-http-base ← downstream of org.apache.camel:camel-core-model
  • camel-http-common ← downstream of org.apache.camel:camel-core-model
  • camel-huawei-parent ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-common ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-dms ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-frs ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-functiongraph ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-iam ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-imagerecognition ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-obs ← downstream of org.apache.camel:camel-core-model
  • camel-huaweicloud-smn ← downstream of org.apache.camel:camel-core-model
  • camel-huggingface ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-cos ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-parent ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-secrets-manager ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watson-discovery ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watson-language ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watson-speech-to-text ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watson-text-to-speech ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watsonx-ai ← downstream of org.apache.camel:camel-core-model
  • camel-ibm-watsonx-data ← downstream of org.apache.camel:camel-core-model
  • camel-ical ← downstream of org.apache.camel:camel-core-model
  • camel-iggy ← downstream of org.apache.camel:camel-core-model
  • camel-ignite ← downstream of org.apache.camel:camel-core-model
  • camel-infinispan ← downstream of org.apache.camel:camel-core-model
  • camel-infinispan-common ← downstream of org.apache.camel:camel-core-model
  • camel-infinispan-embedded ← downstream of org.apache.camel:camel-core-model
  • camel-infinispan-parent ← downstream of org.apache.camel:camel-core-model
  • camel-influxdb ← downstream of org.apache.camel:camel-core-model
  • camel-influxdb2 ← downstream of org.apache.camel:camel-core-model
  • camel-iso8583 ← downstream of org.apache.camel:camel-core-model
  • camel-jackson ← downstream of org.apache.camel:camel-core-model
  • camel-jackson-avro ← downstream of org.apache.camel:camel-core-model
  • camel-jackson-protobuf ← downstream of org.apache.camel:camel-core-model
  • camel-jackson3 ← downstream of org.apache.camel:camel-core-model
  • camel-jackson3-avro ← downstream of org.apache.camel:camel-core-model
  • camel-jackson3-protobuf ← downstream of org.apache.camel:camel-core-model
  • camel-jackson3xml ← downstream of org.apache.camel:camel-core-model
  • camel-jacksonxml ← downstream of org.apache.camel:camel-core-model
  • camel-jactl ← downstream of org.apache.camel:camel-core-model
  • camel-jandex ← downstream of org.apache.camel:camel-core-model
  • camel-jasypt ← downstream of org.apache.camel:camel-core-model
  • camel-java-io ← downstream of org.apache.camel:camel-xml-io
  • camel-java-joor-dsl ← downstream of org.apache.camel:camel-core-languages
  • camel-javascript ← downstream of org.apache.camel:camel-core-model
  • camel-jaxb ← downstream of org.apache.camel:camel-core-model
  • camel-jbang-console ← downstream of org.apache.camel:camel-management
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-xml-io
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-test-junit6
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-test-junit6
  • camel-jcache ← downstream of org.apache.camel:camel-core-model
  • camel-jcr ← downstream of org.apache.camel:camel-core-model
  • camel-jdbc ← downstream of org.apache.camel:camel-core-model
  • camel-jetty ← downstream of org.apache.camel:camel-core-model
  • camel-jetty-common ← downstream of org.apache.camel:camel-core-model
  • camel-jfr ← downstream of org.apache.camel:camel-core-model
  • camel-jgroups ← downstream of org.apache.camel:camel-core-model
  • camel-jgroups-raft ← downstream of org.apache.camel:camel-core-model
  • camel-jira ← downstream of org.apache.camel:camel-core-model
  • camel-jms ← downstream of org.apache.camel:camel-core-model
  • camel-jmx ← downstream of org.apache.camel:camel-core-model
  • camel-jolt ← downstream of org.apache.camel:camel-core-model
  • camel-jooq ← downstream of org.apache.camel:camel-core-model
  • camel-joor ← downstream of org.apache.camel:camel-core-model
  • camel-jpa ← downstream of org.apache.camel:camel-core-model
  • camel-jq ← downstream of org.apache.camel:camel-core-model
  • camel-jsch ← downstream of org.apache.camel:camel-core-model
  • camel-jslt ← downstream of org.apache.camel:camel-core-model
  • camel-json-validator ← downstream of org.apache.camel:camel-core-model
  • camel-jsonapi ← downstream of org.apache.camel:camel-core-model
  • camel-jsonata ← downstream of org.apache.camel:camel-core-model
  • camel-jsonb ← downstream of org.apache.camel:camel-core-model
  • camel-jsonpath ← downstream of org.apache.camel:camel-core-model
  • camel-jsoup ← downstream of org.apache.camel:camel-core-model
  • camel-jt400 ← downstream of org.apache.camel:camel-core-model
  • camel-jta ← downstream of org.apache.camel:camel-core-model
  • camel-jte ← downstream of org.apache.camel:camel-core-model
  • camel-kafka ← downstream of org.apache.camel:camel-core-model
  • camel-kafka-common ← downstream of org.apache.camel:camel-core-model
  • camel-kafka-share ← downstream of org.apache.camel:camel-core-model
  • camel-kamelet ← downstream of org.apache.camel:camel-core-model
  • camel-kamelet-main-support ← downstream of org.apache.camel:camel-main
  • camel-keycloak ← downstream of org.apache.camel:camel-core-model
  • camel-knative ← downstream of org.apache.camel:camel-core-model
  • camel-knative-api ← downstream of org.apache.camel:camel-core-model
  • camel-knative-http ← downstream of org.apache.camel:camel-core-model
  • camel-knative-parent ← downstream of org.apache.camel:camel-core-model
  • camel-kserve ← downstream of org.apache.camel:camel-core-model
  • camel-kubernetes ← downstream of org.apache.camel:camel-core-model
  • camel-kudu ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-agent ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-agent-api ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-chat ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-core ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-embeddings ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-embeddingstore ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-embeddingstore-api ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-ingest ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-tokenizer ← downstream of org.apache.camel:camel-core-model
  • camel-langchain4j-web-search ← downstream of org.apache.camel:camel-core-model
  • camel-language ← downstream of org.apache.camel:camel-core-model
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-ldap ← downstream of org.apache.camel:camel-core-model
  • camel-ldif ← downstream of org.apache.camel:camel-core-model
  • camel-log ← downstream of org.apache.camel:camel-core-model
  • camel-lra ← downstream of org.apache.camel:camel-core-model
  • camel-lucene ← downstream of org.apache.camel:camel-core-model
  • camel-lumberjack ← downstream of org.apache.camel:camel-core-model
  • camel-lzf ← downstream of org.apache.camel:camel-core-model
  • camel-mail ← downstream of org.apache.camel:camel-core-model
  • camel-mail-microsoft-oauth ← downstream of org.apache.camel:camel-core-model
  • camel-main ← downstream of org.apache.camel:camel-core-engine
  • camel-management ← downstream of org.apache.camel:camel-xml-io
  • camel-mapstruct ← downstream of org.apache.camel:camel-core-model
  • camel-master ← downstream of org.apache.camel:camel-core-model
  • camel-maven-plugin ← downstream of org.apache.camel:camel-spring-main
  • camel-mcp-server ← downstream of org.apache.camel:camel-core-model
  • camel-mcp-server-api ← downstream of org.apache.camel:camel-core-model
  • camel-mdc ← downstream of org.apache.camel:camel-core-model
  • camel-metrics ← downstream of org.apache.camel:camel-core-model
  • camel-micrometer ← downstream of org.apache.camel:camel-core-model
  • camel-micrometer-observability ← downstream of org.apache.camel:camel-core-model
  • camel-micrometer-prometheus ← downstream of org.apache.camel:camel-core-model
  • camel-microprofile-config ← downstream of org.apache.camel:camel-core-model
  • camel-microprofile-fault-tolerance ← downstream of org.apache.camel:camel-core-model
  • camel-microprofile-health ← downstream of org.apache.camel:camel-core-model
  • camel-microprofile-parent ← downstream of org.apache.camel:camel-core-model
  • camel-milo ← downstream of org.apache.camel:camel-core-model
  • camel-milvus ← downstream of org.apache.camel:camel-core-model
  • camel-mina ← downstream of org.apache.camel:camel-core-model
  • camel-mina-sftp ← downstream of org.apache.camel:camel-core-model
  • camel-minio ← downstream of org.apache.camel:camel-core-model
  • camel-mllp ← downstream of org.apache.camel:camel-core-model
  • camel-mock ← downstream of org.apache.camel:camel-core-model
  • camel-mongodb ← downstream of org.apache.camel:camel-core-model
  • camel-mongodb-gridfs ← downstream of org.apache.camel:camel-core-model
  • camel-mustache ← downstream of org.apache.camel:camel-core-model
  • camel-mvel ← downstream of org.apache.camel:camel-core-model
  • camel-mybatis ← downstream of org.apache.camel:camel-core-model
  • camel-nats ← downstream of org.apache.camel:camel-core-model
  • camel-neo4j ← downstream of org.apache.camel:camel-core-model
  • camel-netty ← downstream of org.apache.camel:camel-core-model
  • camel-netty-http ← downstream of org.apache.camel:camel-core-model
  • camel-oaipmh ← downstream of org.apache.camel:camel-core-model
  • camel-oauth ← downstream of org.apache.camel:camel-core-model
  • camel-observability-services ← downstream of org.apache.camel:camel-core-model
  • camel-observation ← downstream of org.apache.camel:camel-core-model
  • camel-ocsf ← downstream of org.apache.camel:camel-core-model
  • camel-odata ← downstream of org.apache.camel:camel-core-model
  • camel-ognl ← downstream of org.apache.camel:camel-core-model
  • camel-olingo2 ← downstream of org.apache.camel:camel-core-model
  • camel-olingo2-api ← downstream of org.apache.camel:camel-core-model
  • camel-olingo2-parent ← downstream of org.apache.camel:camel-core-model
  • camel-olingo4 ← downstream of org.apache.camel:camel-core-model
  • camel-olingo4-api ← downstream of org.apache.camel:camel-core-model
  • camel-olingo4-parent ← downstream of org.apache.camel:camel-core-model
  • camel-once ← downstream of org.apache.camel:camel-core-model
  • camel-opa ← downstream of org.apache.camel:camel-core-model
  • camel-openai ← downstream of org.apache.camel:camel-core-model
  • camel-openapi-java ← downstream of org.apache.camel:camel-core-model
  • camel-openapi-rest-dsl-generator ← downstream of org.apache.camel:camel-core-model
  • camel-openapi-validator ← downstream of org.apache.camel:camel-core-model
  • camel-openfeature ← downstream of org.apache.camel:camel-core-model
  • camel-openfga ← downstream of org.apache.camel:camel-core-model
  • camel-opensearch ← downstream of org.apache.camel:camel-core-model
  • camel-openstack ← downstream of org.apache.camel:camel-core-model
  • camel-opentelemetry ← downstream of org.apache.camel:camel-core-model
  • camel-opentelemetry-metrics ← downstream of org.apache.camel:camel-core-model
  • camel-opentelemetry2 ← downstream of org.apache.camel:camel-core-model
  • camel-optaplanner ← downstream of org.apache.camel:camel-core-model
  • camel-paho ← downstream of org.apache.camel:camel-core-model
  • camel-paho-mqtt5 ← downstream of org.apache.camel:camel-core-model
  • camel-parquet-avro ← downstream of org.apache.camel:camel-core-model
  • camel-pdf ← downstream of org.apache.camel:camel-core-model
  • camel-pg-replication-slot ← downstream of org.apache.camel:camel-core-model
  • camel-pgevent ← downstream of org.apache.camel:camel-core-model
  • camel-pgvector ← downstream of org.apache.camel:camel-core-model
  • camel-pinecone ← downstream of org.apache.camel:camel-core-model
  • camel-platform-http ← downstream of org.apache.camel:camel-core-model
  • camel-platform-http-jolokia ← downstream of org.apache.camel:camel-core-model
  • camel-platform-http-main ← downstream of org.apache.camel:camel-core-model
  • camel-platform-http-vertx ← downstream of org.apache.camel:camel-core-model
  • camel-plc4x ← downstream of org.apache.camel:camel-core-model
  • camel-pqc ← downstream of org.apache.camel:camel-core-model
  • camel-printer ← downstream of org.apache.camel:camel-core-model
  • camel-protobuf ← downstream of org.apache.camel:camel-core-model
  • camel-pubnub ← downstream of org.apache.camel:camel-core-model
  • camel-pulsar ← downstream of org.apache.camel:camel-core-model
  • camel-python ← downstream of org.apache.camel:camel-core-model
  • camel-python3 ← downstream of org.apache.camel:camel-core-model
  • camel-qdrant ← downstream of org.apache.camel:camel-core-model
  • camel-quartz ← downstream of org.apache.camel:camel-core-model
  • camel-quickfix ← downstream of org.apache.camel:camel-core-model
  • camel-quickjs ← downstream of org.apache.camel:camel-core-model
  • camel-reactive-streams ← downstream of org.apache.camel:camel-core-model
  • camel-reactor ← downstream of org.apache.camel:camel-core-model
  • camel-redis ← downstream of org.apache.camel:camel-core-model
  • camel-ref ← downstream of org.apache.camel:camel-core-model
  • camel-resilience4j ← downstream of org.apache.camel:camel-core-model
  • camel-resilience4j-micrometer ← downstream of org.apache.camel:camel-core-model
  • camel-resourceresolver-github ← downstream of org.apache.camel:camel-core-model
  • camel-rest ← downstream of org.apache.camel:camel-core-model
  • camel-rest-openapi ← downstream of org.apache.camel:camel-core-model
  • camel-rest-postman ← downstream of org.apache.camel:camel-core-model
  • camel-restdsl-openapi-plugin ← downstream of org.apache.camel:camel-core-engine
  • camel-robotframework ← downstream of org.apache.camel:camel-core-model
  • camel-rocketmq ← downstream of org.apache.camel:camel-core-model
  • camel-rss ← downstream of org.apache.camel:camel-core-model
  • camel-rxjava ← downstream of org.apache.camel:camel-core-model
  • camel-saga ← downstream of org.apache.camel:camel-core-model
  • camel-salesforce ← downstream of org.apache.camel:camel-core-model
  • camel-salesforce-codegen ← downstream of org.apache.camel:camel-core-model
  • camel-salesforce-maven-plugin ← downstream of org.apache.camel:camel-core-model
  • camel-salesforce-parent ← downstream of org.apache.camel:camel-core-model
  • camel-sap-netweaver ← downstream of org.apache.camel:camel-core-model
  • camel-saxon ← downstream of org.apache.camel:camel-core-model
  • camel-scheduler ← downstream of org.apache.camel:camel-core-model
  • camel-schematron ← downstream of org.apache.camel:camel-core-model
  • camel-seda ← downstream of org.apache.camel:camel-core-model
  • camel-semantic ← downstream of org.apache.camel:camel-core-model
  • camel-semantic-openai ← downstream of org.apache.camel:camel-core-model
  • camel-semantic-typesafe-ai ← downstream of org.apache.camel:camel-core-model
  • camel-semantic-wolf-defender ← downstream of org.apache.camel:camel-core-model
  • camel-servicenow ← downstream of org.apache.camel:camel-core-model
  • camel-servicenow-maven-plugin ← downstream of org.apache.camel:camel-core-model
  • camel-servicenow-parent ← downstream of org.apache.camel:camel-core-model
  • camel-servlet ← downstream of org.apache.camel:camel-core-model
  • camel-shell ← downstream of org.apache.camel:camel-core-model
  • camel-shiro ← downstream of org.apache.camel:camel-core-model
  • camel-sjms ← downstream of org.apache.camel:camel-core-model
  • camel-sjms2 ← downstream of org.apache.camel:camel-core-model
  • camel-slack ← downstream of org.apache.camel:camel-core-model
  • camel-smb ← downstream of org.apache.camel:camel-core-model
  • camel-smooks ← downstream of org.apache.camel:camel-core-model
  • camel-smpp ← downstream of org.apache.camel:camel-core-model
  • camel-snakeyaml ← downstream of org.apache.camel:camel-core-model
  • camel-snmp ← downstream of org.apache.camel:camel-core-model
  • camel-soap ← downstream of org.apache.camel:camel-core-model
  • camel-solr ← downstream of org.apache.camel:camel-core-model
  • camel-spiffe ← downstream of org.apache.camel:camel-core-model
  • camel-splunk-hec ← downstream of org.apache.camel:camel-core-model
  • camel-spring ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ai-chat ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ai-embeddings ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ai-image ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ai-parent ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ai-vector-store ← downstream of org.apache.camel:camel-core-model
  • camel-spring-batch ← downstream of org.apache.camel:camel-core-model
  • camel-spring-cloud-config ← downstream of org.apache.camel:camel-core-model
  • camel-spring-jdbc ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ldap ← downstream of org.apache.camel:camel-core-model
  • camel-spring-main ← downstream of org.apache.camel:camel-core-model
  • camel-spring-parent ← downstream of org.apache.camel:camel-core-model
  • camel-spring-rabbitmq ← downstream of org.apache.camel:camel-core-model
  • camel-spring-redis ← downstream of org.apache.camel:camel-core-model
  • camel-spring-security ← downstream of org.apache.camel:camel-core-model
  • camel-spring-ws ← downstream of org.apache.camel:camel-core-model
  • camel-spring-xml ← downstream of org.apache.camel:camel-core-model
  • camel-sql ← downstream of org.apache.camel:camel-core-model
  • camel-ssh ← downstream of org.apache.camel:camel-core-model
  • camel-state-store ← downstream of org.apache.camel:camel-core-model
  • camel-state-store-parent ← downstream of org.apache.camel:camel-core-model
  • camel-stax ← downstream of org.apache.camel:camel-core-model
  • camel-stitch ← downstream of org.apache.camel:camel-core-model
  • camel-stream ← downstream of org.apache.camel:camel-core-model
  • camel-streamcaching-test ← downstream of org.apache.camel:camel-test-junit6
  • camel-stringtemplate ← downstream of org.apache.camel:camel-core-model
  • camel-stripe ← downstream of org.apache.camel:camel-core-model
  • camel-stub ← downstream of org.apache.camel:camel-core-model
  • camel-swift ← downstream of org.apache.camel:camel-core-model
  • camel-syslog ← downstream of org.apache.camel:camel-core-model
  • camel-tahu ← downstream of org.apache.camel:camel-core-model
  • camel-tarfile ← downstream of org.apache.camel:camel-core-model
  • camel-telegram ← downstream of org.apache.camel:camel-core-model
  • camel-telemetry ← downstream of org.apache.camel:camel-core-model
  • camel-telemetry-dev ← downstream of org.apache.camel:camel-core-model
  • camel-tensorflow-serving ← downstream of org.apache.camel:camel-core-model
  • camel-test-citrus ← downstream of org.apache.camel:camel-core-model
  • camel-test-dst ← downstream of org.apache.camel:camel-core-model
  • camel-test-infra-all ← downstream of org.apache.camel:camel-test-infra-artemis
  • camel-test-infra-artemis ← downstream of org.apache.camel:camel-test-junit6
  • camel-test-infra-smb ← downstream of org.apache.camel:camel-test-junit6
  • camel-test-junit5 ← downstream of org.apache.camel:camel-core-model
  • camel-test-junit6 ← downstream of org.apache.camel:camel-core-model
  • camel-test-main-junit5 ← downstream of org.apache.camel:camel-core-model
  • camel-test-main-junit6 ← downstream of org.apache.camel:camel-core-model
  • camel-test-parent ← downstream of org.apache.camel:camel-core-model
  • camel-test-spring-junit5 ← downstream of org.apache.camel:camel-core-model
  • camel-test-spring-junit6 ← downstream of org.apache.camel:camel-core-model
  • camel-thrift ← downstream of org.apache.camel:camel-core-model
  • camel-thymeleaf ← downstream of org.apache.camel:camel-core-model
  • camel-tika ← downstream of org.apache.camel:camel-core-model
  • camel-timer ← downstream of org.apache.camel:camel-core-model
  • camel-toon ← downstream of org.apache.camel:camel-core-model
  • camel-tracing ← downstream of org.apache.camel:camel-core-model
  • camel-twilio ← downstream of org.apache.camel:camel-core-model
  • camel-twitter ← downstream of org.apache.camel:camel-core-model
  • camel-typesafe-ai ← downstream of org.apache.camel:camel-core-model
  • camel-ubl ← downstream of org.apache.camel:camel-core-model
  • camel-undertow ← downstream of org.apache.camel:camel-core-model
  • camel-undertow-spring-security ← downstream of org.apache.camel:camel-core-model
  • camel-univocity-parsers ← downstream of org.apache.camel:camel-core-model
  • camel-validator ← downstream of org.apache.camel:camel-core-model
  • camel-velocity ← downstream of org.apache.camel:camel-core-model
  • camel-vertx ← downstream of org.apache.camel:camel-core-model
  • camel-vertx-common ← downstream of org.apache.camel:camel-core-model
  • camel-vertx-http ← downstream of org.apache.camel:camel-core-model
  • camel-vertx-parent ← downstream of org.apache.camel:camel-core-model
  • camel-vertx-websocket ← downstream of org.apache.camel:camel-core-model
  • camel-wal ← downstream of org.apache.camel:camel-core-model
  • camel-wasm ← downstream of org.apache.camel:camel-core-model
  • camel-weather ← downstream of org.apache.camel:camel-core-model
  • camel-weaviate ← downstream of org.apache.camel:camel-core-model
  • camel-web3j ← downstream of org.apache.camel:camel-core-model
  • camel-webhook ← downstream of org.apache.camel:camel-core-model
  • camel-whatsapp ← downstream of org.apache.camel:camel-core-model
  • camel-wordpress ← downstream of org.apache.camel:camel-core-model
  • camel-workday ← downstream of org.apache.camel:camel-core-model
  • camel-xchange ← downstream of org.apache.camel:camel-core-model
  • camel-xj ← downstream of org.apache.camel:camel-core-model
  • camel-xml-io ← downstream of org.apache.camel:camel-core-model
  • camel-xml-io-dsl ← downstream of org.apache.camel:camel-xml-io
  • camel-xml-jaxb ← downstream of org.apache.camel:camel-core-model
  • camel-xml-jaxb-dsl ← downstream of org.apache.camel:camel-core-languages
  • camel-xml-jaxb-dsl-test-definition ← downstream of org.apache.camel:camel-core-model
  • camel-xml-jaxb-dsl-test-spring ← downstream of org.apache.camel:camel-core-languages
  • camel-xml-jaxp ← downstream of org.apache.camel:camel-core-model
  • camel-xmlsecurity ← downstream of org.apache.camel:camel-core-model
  • camel-xmpp ← downstream of org.apache.camel:camel-core-model
  • camel-xpath ← downstream of org.apache.camel:camel-core-model
  • camel-xslt ← downstream of org.apache.camel:camel-core-model
  • camel-xslt-saxon ← downstream of org.apache.camel:camel-core-model
  • camel-yaml-dsl-common ← downstream of org.apache.camel:camel-core-engine
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-xml-io
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-yaml-io ← downstream of org.apache.camel:camel-core-model
  • camel-zendesk ← downstream of org.apache.camel:camel-core-model
  • camel-zip-deflater ← downstream of org.apache.camel:camel-core-model
  • camel-zipfile ← downstream of org.apache.camel:camel-core-model
  • camel-zookeeper ← downstream of org.apache.camel:camel-core-model
  • camel-zookeeper-master ← downstream of org.apache.camel:camel-core-model
  • components ← downstream of org.apache.camel:camel-core-model
Modules with tests skipped (26)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • core/camel-core: 4 test(s) disabled on GitHub Actions
Build reactor — dependencies compiled but only changed modules were tested (3 modules, 7.8s total)

Total reactor time: 7.8s

Module Duration Status
Camel :: Core Processor 7.8s SUCCESS
Camel :: Core n/a
Camel :: Docs n/a

Top 20 slowest modules:

  • Camel :: Core Processor (7.8s)

⚙️ View full build and test results

@gnodet
gnodet marked this pull request as ready for review October 21, 2026 11:34
oscerd
oscerd previously requested changes Oct 21, 2026

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR

Moving the error-handler start to doStart() fixes the reported Split/Multicast symptom. But gating the runtime start on the CamelContext status leaves error handlers built at runtime unstarted. That breaks Recipient List, which did not have the reported bug, while the context is starting or stopping.

Blockers

  • MulticastProcessor.java:1220: the getCamelContext().isStarted() gate also skips the start of error handlers built at runtime.

Other findings: 5 (non-blocking)

Blocker: error handlers built at runtime stay unstarted

isStarted() is true only in Started (BaseService.java:319-321). So it is false in more phases than doInit(), where the context is Initializing. It is also false while the context is Starting: with the default route controller, each route's consumer runs before the next route starts (InternalRouteStartupManager.java:425,465). And it is false while the context is Stopping: in-flight exchanges finish after stop() has set Stopping (BaseService.java:173, AbstractCamelContext.java:3343).

Split and Multicast pre-build their wrappers in doInit(), so the new doStart() loop covers them. RecipientListProcessor, however, is created with route == null (RecipientList.java:218-219) and builds every wrapper at runtime (RecipientListProcessor.java:370). For a recipient first seen in one of those phases, the DefaultErrorHandler clone is never started. Its taskFactory is only created in doStart() (RedeliveryErrorHandler.java:2159-2168), so process() fails the sub-exchange with a NullPointerException (:191, :200-203). The wrapper is also cached (MulticastProcessor.java:1214), and the cache-hit path returns it without starting it (:1200-1203). So that recipient keeps failing until the route restarts. Before this PR the start was unconditional.

What do you think about gating on this processor's own state instead, which stays false until the processor starts?

if (isRunAllowed()) {
    ServiceHelper.startService(answer);
}

A test that sends through a recipientList while the context is still Starting would cover this, for example from an event notifier on that route's RouteStartedEvent.

Other findings

  1. Upgrade guide (non-blocking). Users will notice this change at startup:

    • With the default route controller, context.start() now fails with a FailedToStartRouteException for the route (RouteService.java:126-132) instead of the producer's own exception.
    • These producers now start with their route, in startupOrder, instead of ahead of all routes.
    • With the supervising controller, the producers of an autoStartup=false route are no longer started with the context.

    A short camel-core note in camel-4x-upgrade-guide-4_24.adoc would help, for example after the pollEnrich note.

  2. Tests (suggestion). DefaultSupervisingRouteControllerMulticastSplitTest only checks the route status, so it also passes without the doStart() hunk. Sending a message once the route is Started and checking that the endpoint saw three start attempts would cover the retry path. Comparing with ServiceStatus.Started avoids the toString() comparison.

  3. Follow-up, pre-existing. doStop() and doShutdown() (MulticastProcessor.java:1311,1316) pass the errorHandlers map. ServiceHelper only handles Service and Iterable (ServiceHelper.java:185-191), so for a map it does nothing, and the wrappers started at :1288 are never stopped. Producers are still stopped through processors (SendProcessor.doStop), so nothing leaks. Passing errorHandlers.values() would make the lifecycle symmetric.

  4. Follow-up, pre-existing. Splitter.doInit() (Splitter.java:130-133) still starts the watermark resumeStrategy and loads its cache during route init. A broker-backed strategy that cannot connect yet would fail the context in the same way. This is worth its own Jira.

  5. Title (suggestion). The PR title becomes the squash commit subject and describes the bug, so in the log it reads as the opposite of the change. Something like "start the Split and Multicast outputs when the route starts" would describe the fix.

Checked and fine:

  • The per-output producers are still started by doStart() through processors (:1284).
  • errorHandlers.values() is a snapshot (SimpleLRUCache.java:314-316), and BaseService.start() does nothing when the service is already started (BaseService.java:116-123), so there is no double start.
  • There is no change to ProducerCache or cacheSize.

Reviewed with Claude Code on behalf of oscerd. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@gnodet gnodet changed the title CAMEL-25823: camel-core - Split and Multicast start child producers before CamelContext starts CAMEL-25823: camel-core - Start Split and Multicast output producers when the route starts Oct 21, 2026
@github-actions github-actions Bot added the docs label Oct 21, 2026
@gnodet

gnodet commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @oscerd!

The second commit (4ac39e9) addresses all the actionable findings — it was pushed just as the review came in:

Blocker — isRunAllowed() gate: Replaced getCamelContext().isStarted() with isRunAllowed() as suggested. This correctly handles the RecipientListProcessor case where wrappers are built at runtime during the Starting and Stopping phases.

Finding 3 — errorHandlers lifecycle: Changed doStop() and doShutdown() to pass errorHandlers.values() instead of the map itself, making the lifecycle symmetric.

Finding 2 — test coverage: The test now sends a message after the route reaches Started and verifies the process count. ServiceStatus.Started is used for comparison (not toString).

Finding 1 — upgrade guide: Added to camel-4x-upgrade-guide-4_24.adoc.

Finding 4 — Splitter watermark (pre-existing): Agreed, this is a separate concern. I'll open a dedicated JIRA.

Finding 5 — PR title: The title already says "Start Split and Multicast output producers when the route starts" which describes the fix. Happy to adjust if there is a preferred wording.

CI is running on the updated commit.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the prior review (posted under oscerd) — the second commit addresses most of the raised points.

What's addressed:

  • ✅ Blocker: isRunAllowed() gate replaces getCamelContext().isStarted(). isRunAllowed() returns true in STARTING/STARTED/SUSPENDED states, so error handlers built at runtime during Starting (e.g. RecipientListProcessor) are correctly started immediately. During doInit() (INITIALIZING state), isRunAllowed() is false, so wrappers are deferred to doStart() as intended.
  • ✅ doStop()/doShutdown() now pass errorHandlers.values() — fixes the silent no-op that left error handler wrappers never stopped.
  • ✅ Upgrade guide section added with accurate description of the three behavioral changes.
  • ✅ Tests now assert start attempt counts (assertEquals(3, endpoint.startAttempts.get())) and message processing counts — and use ServiceStatus.Started for the status comparison.
  • ✅ PR title updated to describe the fix, not the bug.

One test bug remains:

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — all issues have been addressed.

What's fixed in the 3rd commit (0fd6dbe):

  • ✅ Test delimiter bug: template.sendBody("direct:split", "a b c") → "a,b,c" — the space-delimited body produced a singleton under split(body())'s default comma delimiter; the processCount assertion would have always failed.

All prior findings (oscerd + gnodet-bot) are resolved:

  • ✅ isRunAllowed() gate replaces getCamelContext().isStarted() — correctly covers STARTING/STARTED/SUSPENDING/SUSPENDED phases, so error handlers built at runtime by RecipientListProcessor are started immediately, while those built during doInit() (INITIALIZING phase) are deferred to doStart().
  • ✅ doStart() starts pre-built error handlers via ServiceHelper.startService(errorHandlers.values()) — idempotent (no-op if already started).
  • ✅ doStop()/doShutdown() now pass errorHandlers.values() — the prior code passing the Map directly was a silent no-op.
  • ✅ Upgrade guide documents all three behavioral changes accurately.
  • ✅ Test assertions strengthened: startAttempts == 3 validates the retry count, processCount validates end-to-end routing. Both gate correctly on the fix being present.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Guillaume. Deferring the wrapper start to doStart() is the right fix for the supervising controller, and gating on the processor's own state (isRunAllowed()) addresses Andrea's recipientList concern. I went through the other users of MulticastProcessor: RecipientListProcessor (one instance per RecipientList, started in its doStart()), the SEDA multipleConsumers multicast (started before use) and Splitter (delegates to super). They are fine. Actually stopping the wrappers now (via values()) is safe too: RedeliveryErrorHandler has no doStop() and NoErrorHandler.doStop() is a no-op, so outputs and the shared dead letter processor are not stopped on a route stop, and doStart() starts the wrappers again on restart.

Things to address:

  1. Blocking: the wrapper is now put in the cache before it is started, which opens a race for wrappers built at runtime (see inline).
  2. Lines 1313 and 1318 are over the formatter's 128-character limit, so the build will reformat them and the uncommitted-changes check will fail. Please run mvn formatter:format impsort:sort in core/camel-core-processor.
  3. Two of the upgrade guide bullets do not match what happens (see inline).
  4. The PR description still describes the getCamelContext().isStarted() gate. Please update it to the isRunAllowed() version.

Optional: a test that sends through a recipientList to a recipient not seen before, from a few threads at once, would cover the runtime path that this change touches.

This review was generated by an AI agent (Claude Code on behalf of davsclaus) and may contain inaccuracies. Please verify all suggestions before applying.

Comment thread docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc Outdated
Comment thread docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc Outdated

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review and davsclaus's review — all issues raised by both have been addressed in the 4th commit.

davsclaus findings resolved:

  • ✅ Cache-before-start race: ServiceHelper.startService(answer) is now called before errorHandlers.putIfAbsent(key, answer), restoring the original guarantee that no concurrent exchange can observe an unstarted wrapper via the cache-hit path. The comment documents this explicitly.
  • ✅ Line length: doStop() and doShutdown() calls are now wrapped across multiple lines (well within the 128-char formatter limit).
  • ✅ Upgrade guide bullet 1 reframed: now focuses on when producers start rather than error behavior, with the init-only context example davsclaus suggested.
  • ✅ Upgrade guide bullet 3: autoStartup=false note now correctly says "regardless of which route controller is used".
  • ✅ PR description updated to describe the isRunAllowed() gate.

Minor nit (non-blocking): The inline comment says isRunAllowed() covers "STARTING/STARTED/SUSPENDED phases" but the actual implementation (status >= STARTING && status <= SUSPENDED) also includes SUSPENDING. The gate behavior is correct; only the comment is slightly incomplete.

LGTM. Solid fix for a tricky lifecycle ordering issue.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous findings are all addressed. The nit I raised in the last review (comment missing SUSPENDING phase) is fixed in commit f783b705.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 0fd6dbe..f783b70. Thanks Guillaume, all my points are addressed.

  1. The wrapper is now started before it is put into errorHandlers, in the same order as on main, so the cache-hit path can no longer hand out an unstarted wrapper built at runtime. Wrappers built in doInit() are still cached unstarted and started in doStart(). Two threads that miss the cache for the same new recipient both start a wrapper and one wins putIfAbsent; the other is used for its own exchange and then dropped, which is unchanged from main and harmless, since starting a RedeliveryErrorHandler only builds its task factory.
  2. The doStop()/doShutdown() lines are wrapped within the formatter limit.
  3. The upgrade guide bullets now describe the actual change: the producers start with the route instead of at context initialization, and the autoStartup=false note applies with either route controller.
  4. The PR description now describes the isRunAllowed() gate. Small leftovers, if you touch it again: it omits SUSPENDING from the phases, and it does not mention the errorHandlers.values() stop/shutdown fix or the upgrade-guide entry.

LGTM once CI is green.

This review was generated by an AI agent (Claude Code on behalf of davsclaus) and may contain inaccuracies. Please verify all suggestions before applying.

gnodet and others added 5 commits October 23, 2026 11:41
…ssor from doInit to doStart

MulticastProcessor.doInit() calls wrapInErrorHandler() for each child processor,
which eagerly called ServiceHelper.startService(answer) on the wrapped error handler.
This started the child's SendProcessor (and its producer) before CamelContext was
fully started, outside the SupervisingRouteController retry scope. A producer that
cannot connect yet (e.g. broker is down) would propagate the exception directly to
context.start(), failing the entire context.

Fix: defer the ServiceHelper.startService(answer) call in wrapInErrorHandler to when
the context is already fully started (isStarted()). When wrapInErrorHandler is called
during doInit() (context is STARTING), the built error handler is cached but not started.
doStart() then explicitly starts any pre-cached error handlers via
ServiceHelper.startService(errorHandlers.values()), which runs under the route lifecycle
managed by the SupervisingRouteController.

Split extends MulticastProcessor and is also fixed by this change.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@davsclaus
davsclaus force-pushed the camel-25823-multicast-split-deferred-start branch from f783b70 to 5074998 Compare October 23, 2026 09:46
…is stopped

When MulticastProcessor.doStop() correctly stops its cached error handlers
(the Map-to-values() fix), a SimpleTask handling an inflight exchange that
was interrupted by a route stop goes through runNotAllowed() instead of
runInterrupted(). Unlike runInterrupted(), runNotAllowed() did not call
logCutOffByRouteStop(), so the expected WARN was never emitted.

Fix: add logCutOffByRouteStop() call in runNotAllowed() for the case where
the exchange already carries a RouteStoppingException (it came from an actual
interruption, not from a never-started exchange) and is not marked as
interrupted (interrupted sub-exchanges are logged via the route-level error
handler, avoiding double-logging).
@gnodet

gnodet commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the RouteStopCutOffLogTest regression introduced by the doStop() correction.

Root cause: The PR correctly fixes MulticastProcessor.doStop() to pass errorHandlers.values() instead of the raw Map — this makes ServiceHelper.stopService(...) actually stop the cached per-branch error handlers (previously it was silently a no-op because Map doesn't implement Iterable).

However, this exposed a gap in SimpleTask.runNotAllowed(). The execution path that now applies when an error handler is properly stopped:

  • Before fix (main): isStoppingOrStopped()=false → isInterrupted()=true → runInterrupted() → sets routeStop=true, propagates up to route-level error handler → route-level handler calls logCutOffByRouteStop() → WARN logged ✓
  • After fix (PR, broken): isStoppingOrStopped()=true → runNotAllowed() → does NOT call logCutOffByRouteStop() → no WARN ✗
  • After this commit: runNotAllowed() now calls logCutOffByRouteStop() when the exchange already carries a RouteStoppingException (real interruption) and is not marked as interrupted (to avoid double-logging vs the interrupted sub-exchange path).

All 3 tests in RouteStopCutOffLogTest pass locally (verified with mvn test -pl core/camel-core).

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous findings are all addressed. The new commit (895e821) fixes the cut-off WARN regression cleanly.

What was fixed:

  • ✅ RedeliveryErrorHandler.runNotAllowed(): adds logCutOffByRouteStop() call when the exchange already carries a RouteStoppingException and is not marked as interrupted — this restores the WARN that was dropped when doStop() began correctly stopping cached error handlers. The !isInterrupted() guard correctly avoids double-logging for interrupted sub-exchanges, which are already logged through their own path.

Pre-fix validation (static): ⚠️ Dynamic check skipped (build takes too long for CI-pending PR). The pre-existing RouteStopCutOffLogTest.theCutOffIsOneWarnLine() exercises exactly this scenario: a Split route is stopped while an exchange is inflight; the test asserts exactly one WARN line and no exhausted failure. Per gnodet's PR comment, this test was regressing before the fix.

No new issues found. The condition is sound, logCutOffByRouteStop guards internally with isCutOffByRouteStop(e) so it's a no-op for non-RouteStoppingException exceptions, and the call happens before taskFactory.release()/cb.done() (correct ordering).

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR

Comment: my approval stands for the rest. Two questions on the new commit 895e821244, which logs the cut-off WARN in SimpleTask.runNotAllowed().

Blockers

  • None, but please check the null logger below.

Other findings: 2 questions, 1 nit.

  • RedeliveryErrorHandler.java:868: logger.getLevel() has no null check. LegacyDefaultErrorHandlerReifier.java:46 passes a null logger when none is configured, and logFailedDelivery guards logger == null for that reason. Add logger != null to the condition.
  • Can it log more than once? Each nested EIP has its own channel error handler, and after a forced stop an exchange on its way out can pass several stopped ones. With an exchange that is not interrupted and keeps its RouteStoppingException, each of them now logs. For example, the theCutOffIsOneWarnLine route with the split inside a filter, or an inner handler that already logged through logFailedDelivery. I have reasoned this from the code and not run it. Marking the exchange after logging (as DirectProducer does with setInterrupted(true)), or skipping exhausted exchanges, plus a nested variant of the test asserting one WARN line, would settle it.
  • Nit: the comment says "this is the route-level exchange", but the branch runs in any stopped error handler. Also, please add a line about this change to the PR description.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

- Add 'logger != null' guard (matching logFailedDelivery) to avoid NPE
  when LegacyDefaultErrorHandlerReifier passes a null logger
- Mark exchange as interrupted after logging the cut-off WARN so that
  nested stopped error handlers do not each emit their own WARN line
  for the same exchange (ensures theCutOffIsOneWarnLine stays at 1)

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review and davsclaus's comment on commit 895e821244 — all three points raised are addressed by the new commit (41c171162).

davsclaus findings resolved:

  • ✅ Null-logger guard: logger != null added to the condition in runNotAllowed() — matches the existing guard in logFailedDelivery, correctly protects against LegacyDefaultErrorHandlerReifier passing null.
  • ✅ Single-WARN guarantee: exchange.getExchangeExtension().setInterrupted(true) is now set after the first logCutOffByRouteStop() call, ensuring that nested stopped error handlers (e.g. a split inside a filter) do not each emit their own WARN for the same exchange. The !isInterrupted() guard in the condition makes this self-consistent.
  • ✅ Comment accuracy: the comment no longer says "route-level exchange" — it correctly describes the branch as running in any stopped error handler, not just the route-level one.
  • ✅ PR description updated: section 4 ("Null-logger guard and single-WARN guarantee") was added.

Pre-fix validation (static): ⚠️ Dynamic check skipped (build time). Static trace confirms the condition is sound: the outer logger != null prevents NPE in the logger.getLevel() call; the !isInterrupted() guard prevents double-logging; logCutOffByRouteStop() is a no-op for non-RouteStoppingException exceptions. The existing theCutOffIsOneWarnLine test covers exactly this scenario.

Previous findings are all addressed. LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's latest review — all three points are addressed in commit 41c171162c96.

davsclaus findings resolved:

  • ✅ Null-logger guard: logger != null added to the condition at line 863 of runNotAllowed(), matching the same guard in logFailedDelivery (line 990). Avoids NPE when LegacyDefaultErrorHandlerReifier passes a null logger.
  • ✅ Double-logging concern: exchange.getExchangeExtension().setInterrupted(true) is called immediately after logging the cut-off WARN. Subsequent stopped error handlers (e.g. in a split-inside-filter scenario) see isInterrupted() == true and skip the branch entirely — only one WARN is emitted per exchange. The existing theCutOffIsOneWarnLine test validates the single-WARN guarantee; a nested variant would add more confidence but is left optional per the review.
  • ✅ Comment nit: the comment no longer says "this is the route-level exchange" — it now reads "the exchange is passing through a stopped error handler" which is accurate for nested EIP handlers as well.
  • ✅ PR description: updated to describe the null-logger guard and the setInterrupted(true) guarantee (item 4 in the Fix section).

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 895e821..41c1711. Thanks Guillaume. The null-logger guard (RedeliveryErrorHandler.java:863) and the comment are fixed, and the description now covers this change. My approval of the Multicast/Split part stands; it is unchanged since f783b70.

One question about RedeliveryErrorHandler.java:869-870 before I approve again: setInterrupted(true) is more than a log marker. A running error handler checks it first (run(), line 466) and takes runInterrupted(), which skips onException, redelivery and the dead letter channel and keeps the exception.

Example: route A (running, with a DLC) calls direct:b, and route B is force-stopped mid-exchange.

  • Before: A's error handler handled the RouteStoppingException (DLC, one WARN via logFailedDelivery).
  • Now: B's stopped handler logs and marks the shared exchange, so A skips the DLC and its consumer rolls back.

That may be the intended CAMEL-25502 meaning (DirectProducer does the same), but here it comes from deduplicating a log line, and it only happens when the handler has a logger. Could we either use a dedicated "cut-off logged" marker, or keep this behaviour and state it in the description and the 4.24 upgrade guide, with a test for the calling-route case? I traced this from the code and have not run it.

Optional: copyResults/MulticastProcessor:972 do not carry interrupted from a sub-exchange to the parent. A cut-off that does not start as interrupted (a slow .process() inside the split rather than the direct wait) may therefore log once for the sub-exchange and once for the parent. RouteStopCutOffLogTest.theCutOffIsOneWarnLine only covers the DirectProducer path, and it counts after the first message arrives. A variant that waits for the route to stop before counting would settle it.

Reviewed with Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

…(true) behavior in upgrade guide

- Add RouteStopCutOffLogNestedTest.nestedEipCutOffIsOneWarnLine(): a multicast
  wrapping a split with an inflight exchange blocked on direct:shipment. When the
  route is stopped, asserts exactly one WARN line — verifying that the
  setInterrupted(true) marker prevents nested stopped error handlers from each
  logging their own WARN for the same exchange.
- Expand the upgrade guide entry to document the setInterrupted(true) consequence:
  an inflight exchange cut off through a stopped Split/Multicast error handler
  skips redelivery, onException routes, and the dead letter channel — same
  behavior as DirectProducer (CAMEL-25502) on a forced route stop.
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

✅ Generated files are up to date

An earlier CI run reported uncommitted generated changes; the latest run no longer does.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's latest review — all four points are addressed in this commit.

  • ✅ Upgrade guide narrowness: the note now correctly lists all stopped error handlers (split, multicast, filter, choice, ...) instead of "Split or Multicast".
  • ✅ Wrong API in doc: the upgrade guide now uses exchange.getExchangeExtension().setInterrupted(true) correctly.
  • ✅ Caller-route test: RouteStopCutOffCallerRouteTest added, covering the DLC-not-called behavior davsclaus requested.
  • ✅ Flaky inflight-detection: RouteStopCutOffLogNestedTest changed from e.getNodeId() to e.getToUri() for the await condition.

One nit in the new test (inline).

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Use e.getToUri() instead of e.getNodeId() for the await condition,
consistent with the same fix applied to RouteStopCutOffLogNestedTest.
getToUri() is set as soon as DirectProducer picks up the exchange,
while getNodeId() may not be set yet on a slow agent.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — all previous findings are resolved.

New commits since last review:

  • 39d7b21 — Merge upstream/main (routine rebase, no code changes)
  • db0c911 — Fix flaky inflight detection in RouteStopCutOffCallerRouteTest

db0c911 assessment:
✅ Flaky inflight detection fix: e.getNodeId() → e.getToUri() — identical to the fix applied to RouteStopCutOffLogNestedTest in the prior commit. getToUri() is set as soon as DirectProducer picks up the exchange, while getNodeId() may not be set on a slow agent. Correct and consistent.

Remaining status:

  • ✅ All davsclaus findings from the Oct 25 review are addressed (upgrade guide broadened, caller-route DLC test added, flaky-test detection fixed)
  • ✅ All code in MulticastProcessor.java and RedeliveryErrorHandler.java unchanged since davsclaus's approval
  • ✅ No new issues introduced

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

…utOffCallerRouteTest

InflightRepository.InflightExchange does not expose a getToUri() method.
The gnodet-bot suggestion incorrectly referenced that API. The correct
condition is getNodeId() == "shipment", which matches the .id("shipment")
annotation on the direct:shipment step — consistent with the equivalent
condition already used in RouteStopCutOffLogNestedTest.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — all previous findings resolved.

New commit (9c49594):
✅ getToUri() → getNodeId() correction — my previous suggestion referenced getToUri(), which does not exist on InflightRepository.InflightExchange (only getNodeId() is exposed). The author correctly reverted to getNodeId(), which matches the .id("shipment") annotation on the direct:shipment step and is already used consistently in RouteStopCutOffLogNestedTest. The fix is correct.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 review — all 4 points are addressed.

davsclaus findings resolved:

  • ✅ Upgrade guide scope: the note now lists all stopped EIP handlers (split, multicast, filter, choice, ...) instead of just "Split or Multicast". The correct API (exchange.getExchangeExtension().setInterrupted(true)) is used. The calling-route consequence (caller route skips onException, redelivery, and DLC) is fully documented, matching davsclaus's suggested wording.
  • ✅ Caller-route test: RouteStopCutOffCallerRouteTest.callerRouteDlcNotCalledOnNestedEipCutOff() pins down that route A's DLC mock is NOT called when route B's nested EIP is force-stopped. This exactly covers the scenario davsclaus requested.
  • ✅ Flaky test fix: RouteStopCutOffLogNestedTest now waits for both getRouteStatus("nested").isStopped() and getInflightRepository().browse().isEmpty() before counting WARNs — the exact fix davsclaus asked for. The getNodeId() condition is also correct (.id("shipment") annotates the direct:shipment step).
  • ✅ Regen commit (ba04753): purely Javadoc line-wrapping reformatting in RouteStopCutOffCallerRouteTest.java. Zero code changes.

One remaining latent gap (non-blocking, pre-existing): ExchangeHelper.copyResults() does not propagate the interrupted flag from sub-exchanges to their parent. If a slow .process() step (rather than a DirectProducer wait) is cut off inside a nested EIP, the route-level error handler may emit a second WARN because the original exchange lacks the interrupted flag set on the sub-exchange. This was davsclaus's optional concern from Oct 25 — it's a pre-existing limitation of copyResults, not a regression from this PR, and is cosmetic (double WARN, no data-loss risk).

LGTM. The lifecycle fix (MulticastProcessor/RedeliveryErrorHandler) is solid and all review concerns are addressed.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of c6d590d090..ba0475349e. Thanks Guillaume. The upgrade-guide paragraph (camel-4x-upgrade-guide-4_24.adoc:352-361) now describes the change as it really is: any nested EIP of the stopped route, the right getExchangeExtension().setInterrupted(true) API, and the effect on a calling route. MulticastProcessor and RedeliveryErrorHandler are unchanged, the merge of main did not touch the PR's own changes, and the Regen commit only reflows the new test's javadoc. One problem is left before I approve: I don't think the new caller-route test can pass yet.

1. RouteStopCutOffCallerRouteTest waits for an interrupt that never comes (:56-72). Route B is consumed from direct:b, so the thread blocked on direct:shipment is route A's seda:caller consumer thread, waiting in DirectComponent.getConsumer(). DirectProducer only turns that wait into a RouteStoppingException when the thread is interrupted. A forced stop of the direct-consumed route B only stops its DirectConsumer, and nothing interrupts route A's thread. The existing cut-off tests work because they stop the seda route itself, which shuts down its executor. So I expect the exchange to stay inflight for the 30 s direct timeout and the 5 s wait at :66-68 to time out. After that the sub-exchange fails with DirectConsumerNotAvailableException rather than a RouteStoppingException, and route A's DLC would get it. The test has not run in CI yet: db0c911 failed to compile on getToUri(), 9c49594 stopped at the regen check, and the run on ba04753 is still building.

Also, expectedMessageCount(0) plus assertIsSatisfied() passes at once, so the test never checks what actually happened to the exchange. Could we make it deterministic and positive? For example:

  • route A is from("direct:caller") with deadLetterChannel("mock:dlc")
  • in route B, inside multicast().split(), a .process() counts down an entered latch and then waits on a release latch, with a timeout
  • the test calls template.asyncSend("direct:caller", ...), waits for entered, calls stopRoute("nested-b"), then releases the latch
  • then it asserts that f.get(10, SECONDS) holds a RouteStoppingException and is interrupted, and that mock:dlc received nothing

That also covers the cut-off of a slow .process() that does not start out interrupted, which I asked about before.

2. The nested test wait (RouteStopCutOffLogNestedTest.java:82-86). Waiting for the inflight repository to drain before counting fixes the early count, thanks. It does not explain the flake we actually saw (Run 1 of 40092170771: no WARN at all within 5 s); with this change that case fails as assertEquals(1, 0) instead. The stricter wait may also expose a second WARN line: the interrupted flag is not carried from a sub-exchange to its parent (MulticastProcessor:972/977, ExchangeHelper.doCopyResults), so the multicast's stopped handler may log again after the split's handler. The current CI run will tell us.

3. Optional. The paragraph at :352 sits under "Split and Multicast child producers start with their route" (:341), but it applies to any nested EIP. Its own === camel-core - ... heading would make it easier to find.

Claude Code on behalf of Claus Ibsen. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

… trigger runNotAllowed()

The previous test stopped only route B (direct:b consumer). Stopping a
DirectConsumer does not interrupt the thread that is blocked inside it,
so the inflight exchange never completed and the await timed out on all
three retries.

The new design:
1. Route B is direct:b → multicast().split().process(latchProcessor)
   The processor counts down 'entered' and blocks via Thread.sleep(MAX_VALUE).
2. The test waits for 'entered', then stops route B first (MulticastProcessor
   and its nested error-handler wrappers are now stopped).
3. Stopping route A (seda:caller) triggers force-shutdown after 1 s,
   which interrupts the blocked thread via shutdownNow().
4. The processor converts InterruptedException → RouteStoppingException so
   that route B's stopped wrappers invoke runNotAllowed(), log one WARN,
   and set interrupted=true on the shared exchange.
5. Route A's DLC error handler sees interrupted=true → runInterrupted()
   → skips redelivery and the DLC.

Test verified locally: passes in ~2.7 s, deterministic.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous findings are all addressed. The new test design is correct and sound — the latch + interrupt approach eliminates the flakiness that davsclaus identified. Two minor robustness nits on the test.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous findings are all addressed. The two nits from my last review are fixed in commit bc13f497.

Nit 1 — assertTrue on latch await: ✅ assertTrue(entered.await(5, TimeUnit.SECONDS), "Processor did not enter within 5 seconds") — the return value is now checked with a clear failure message, exactly as suggested.

Nit 2 — TimeoutException not caught: ✅ } catch (java.util.concurrent.TimeoutException e) { fail("Exchange did not complete within 10 seconds — likely a hang in the error handler chain", e); } — the timeout case now fails with a diagnostic message rather than propagating as an uncaught checked exception.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of ba0475349e..bc13f497e3. Thanks Guillaume. Only RouteStopCutOffCallerRouteTest changed; MulticastProcessor and RedeliveryErrorHandler are unchanged. The previous CI failure (:66 ConditionTimeout on all 3 attempts) was the direct-consumer stop that never interrupts route A's thread. The new version gets its interrupt from route A's seda executor (SedaConsumer.doStop() -> shutdownNow), so I expect it to pass.

One thing is left before I approve: the test no longer tests the behaviour the upgrade guide and its own javadoc describe.

1. The caller route is stopped too, so the test can't tell why the DLC was skipped (RouteStopCutOffCallerRouteTest.java:88). The class javadoc (:38) and the 4.24 upgrade-guide paragraph are about "a calling route A that is still running". The test now calls stopRoute("caller-a"), which stops route A's channel error handler. A stopped RedeliveryTask skips the DLC on its own (run() -> !isRunAllowed(), RedeliveryErrorHandler.java:1131-1138). So the assertion at :104 passes whether or not the interrupted flag did its job.

2. The test throws the RouteStoppingException itself (:132). The framework never turns a user processor's interrupt into a RouteStoppingException, so the test checks the marker logic only for an exception it made itself.

Could we keep route A running and let the framework create the cut-off? For example:

  • route A stays from("seda:caller") (or direct:caller) with deadLetterChannel("mock:dlc"), and is not stopped
  • inside B's multicast().split(): a .process() that counts down entered and then waits on a release latch (with a timeout), followed by .to("mock:afterBlock")
  • the test calls assertTrue(entered.await(5, SECONDS)), then stopRoute("nested-b"), then release.countDown()

The stopped channel of mock:afterBlock then sets the RouteStoppingException in runNotAllowed(), and the running route A should take the interrupted path. Then assert the RouteStoppingException, that mock:dlc is empty, and that mock:afterBlock received nothing. This also removes the Thread.sleep(Long.MAX_VALUE) at :129 (CLAUDE.md: no Thread.sleep() in new tests; a latch await is interruptible too).

3. Small things.

  • Thanks for checking the entered.await(...) result now (:79).
  • :99 catches java.util.concurrent.TimeoutException by its fully qualified name; please import it instead (CLAUDE.md "Import Style").
  • Optional: the interrupted paragraph still sits under the Split/Multicast heading in the upgrade guide.

Blast radius. I grepped core, components and dsl for tests that need Split/Multicast producers started at context initialization. The only hits are autoStartup(false) routes (sql, stream, solr, file split tests, SplitterStreamingUoWIssueTest), and each starts its route before sending. Nothing depends on the old behaviour.

@oscerd: your CHANGES_REQUESTED from 2026-10-21 is still the blocking state. Your blocker is addressed (isRunAllowed() is the gate at MulticastProcessor.java:1217, and the wrapper is started before it is cached), and findings 1, 2, 3 and 5 are in the PR too. Could you re-review or dismiss it? The Splitter resumeStrategy follow-up (your 4) has no JIRA yet as far as I can see.


Reviewed with Claude Code on behalf of @davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

… use latch release to trigger runNotAllowed() naturally; split upgrade guide entry
…() create RouteStoppingException

The previous design (bc13f49) stopped both routes and had the processor throw
RouteStoppingException manually. davsclaus correctly identified two problems:

1. The caller route (caller-a) was stopped too, so the DLC was skipped by route A's
   own stopped error handler, not by the interrupted flag — the test did not verify
   the documented behaviour.

2. The processor threw RouteStoppingException itself; the framework never converts a
   user interrupt to RouteStoppingException, so the test did not exercise that path.

New design:
- Route A (seda:caller) is NOT stopped — it stays running throughout.
- The processor blocks on a release latch and exits normally (no throw, no interrupt).
- A 1 s shutdown timeout is set so stopRoute("nested-b") force-stops after 1 s:
  MulticastProcessor.doStop() is called, which stops the nested error-handler wrappers.
  The seda:caller thread is NOT interrupted (direct:b owns no executor).
- After stopRoute returns, the latch is released. The processor exits normally.
  mock:afterBlock's stopped error-handler wrapper invokes runNotAllowed(), which sets
  a RouteStoppingException on the exchange and marks it as interrupted.
- Route A's DLC error handler sees interrupted=true → runInterrupted() → skips DLC.

This matches the behaviour described in the class javadoc and the 4.24 upgrade guide.
Also imports TimeoutException (previously used by fully qualified name).

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 08:52 review — all three points are addressed in commit ef115368.

davsclaus findings resolved:

  • ✅ Caller route stays running: route A (caller-a) is no longer stopped. Only stopRoute("nested-b") is called, so route A remains live throughout. A 1 s shutdown timeout is set so that stopRoute("nested-b") force-stops after 1 s (the direct:b consumer owns no thread of its own; the seda:caller executor thread is NOT interrupted by the force-stop). After stopRoute returns, MulticastProcessor.doStop() has already stopped the nested error-handler wrappers.
  • ✅ Framework creates the RouteStoppingException: the processor no longer throws it manually. The processor counts down entered, then blocks on a release latch and exits normally. After the latch is released, the next step (mock:afterBlock) is in a stopped error-handler wrapper that invokes runNotAllowed(), which sets the RouteStoppingException on the exchange and calls setInterrupted(true) — exactly the path the upgrade guide and the test javadoc describe.
  • ✅ TimeoutException imported: java.util.concurrent.TimeoutException is now imported; the fully-qualified name in the catch clause is gone.

What the test now actually verifies: the interrupted flag set by the nested stopped wrapper propagates back to the running route A's error handler (runInterrupted() → skips DLC), which is the documented behaviour the class javadoc and the 4.24 upgrade-guide paragraph describe.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 08:52 review — all three points are addressed in commit ef115368.

davsclaus findings resolved:

  • ✅ Caller route stays running: route A (caller-a) is no longer stopped. Only stopRoute("nested-b") is called, so route A remains live throughout. A 1 s shutdown timeout is set so that stopRoute("nested-b") force-stops after 1 s (the direct:b consumer owns no thread of its own; the seda:caller executor thread is NOT interrupted by the force-stop). After stopRoute returns, MulticastProcessor.doStop() has already stopped the nested error-handler wrappers.
  • ✅ Framework creates the RouteStoppingException: the processor no longer throws it manually. The processor counts down entered, then blocks on a release latch and exits normally. After the latch is released, the next step (mock:afterBlock) is in a stopped error-handler wrapper that invokes runNotAllowed(), which sets the RouteStoppingException on the exchange and calls setInterrupted(true) — exactly the path the upgrade guide and the test javadoc describe.
  • ✅ TimeoutException imported: java.util.concurrent.TimeoutException is now imported; the fully-qualified name in the catch clause is gone.

What the test now actually verifies: the interrupted flag set by the nested stopped wrapper propagates back to the running route A's error handler (runInterrupted() → skips DLC), which is the documented behaviour the class javadoc and the 4.24 upgrade-guide paragraph describe.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of bc13f497e3..ef11536884. Thanks Guillaume. Only the caller test and the upgrade guide changed, and MulticastProcessor and RedeliveryErrorHandler are as before, so my approval of that part stands.

The caller test now does what I asked:

  • Route A stays running, and route B's stopped handlers create the RouteStoppingException themselves.
  • stopRoute returns only after stopRouteService has stopped B's handlers (AbstractCamelContext.java:1516-1519), so releasing the latch afterwards is deterministic.
  • Route A then ends in isDone() on the interrupted flag (RedeliveryErrorHandler.java:302-310).
  • The test really checks something: if the DLC had handled the exchange, the future would succeed and the test would fail.
  • The Thread.sleep and the FQCN are gone, and the interrupted paragraph has its own heading.

One thing is left before I approve:

1. RouteStopCutOffLogNestedTest flaked again.

  • On this head, Run 1 failed at :89 with [] ==> expected: <1> but was: <0> and passed on the rerun (run 40179096494, incremental-test / flakes artifacts).
  • That is two failed first attempts in the last three runs that reached the tests (c6d590d090 and ef11536884).
  • I think the cause is the stop order. A forced stopRoute first interrupts the seda thread in shutdownRoutesNow (DefaultShutdownStrategy.java:255). Only afterwards does stopRouteService stop the route's error handlers (AbstractCamelContext.java:1519). So the interrupted exchange unwinds through handlers that may or may not be stopped yet.
  • The latch shape the caller test now uses avoids that: block in a .process(), call stopRoute, then release. RouteStopCutOffLogTest already covers the DirectProducer wait.
  • The test is new in this PR, so it is best fixed here.

2. Optional: count the WARNs in the caller test.

  • The caller test is the slow-.process() cut-off, so it can settle the single-WARN claim in the upgrade guide (:352-357).
  • The interrupted flag belongs to one exchange instance, and copyResults does not carry it over from the split and multicast copies (MulticastProcessor.java:977).
  • As I read the code, the split's per-item wrapper, the split's channel and B's multicast step may each log once. I traced that but have not run it.
  • A ConsumingAppender count like the one in the nested test would tell. If it is more than one, either copy the flag in doDone, or narrow that sentence.

3. Nit: the javadoc and comments name the wrong handler (RouteStopCutOffCallerRouteTest.java:41-42, :52-54, :65-67, :97-101, :140-142).

  • When the latch is released, SimpleTask.done() re-enters run() on the .process() step's own stopped channel (RedeliveryErrorHandler.java:451-465).
  • That handler creates the exception but does not mark it (:861-862). The pipeline then stops, so mock:afterBlock's handler is never entered.
  • The marking happens in an outer stopped handler that finds the exception already set (:863-871).
  • Also, :105 says runInterrupted(), but route A has a DLC, so it runs a RedeliveryTask and the path is isDone().
  • The assertions themselves are right.

4. Nit: release.await(30, TimeUnit.SECONDS) at :152 ignores the return value, so a timed-out wait would continue silently.

CI is green on this head, and the caller test passed on its first attempt.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

…uteTest

RouteStopCutOffLogNestedTest: redesign to eliminate flakiness.

The previous design blocked on direct:shipment (no consumer) and waited
for inflight browse to detect the exchange. The forced stopRoute first
interrupted the SEDA thread, then stopped route services; the exchange
could unwind through handlers that were not yet stopped, so the WARN
count was non-deterministic.

New design mirrors RouteStopCutOffCallerRouteTest: a seda:start route
(route A) forwards to direct:nested (route B) via direct (no thread
switch). A .process() inside route B's filter counts down an entered
latch and waits on a release latch. The test waits for entered, stops
route B (seda:start thread is NOT interrupted — direct: has no executor
thread), then releases the latch. The processor exits normally; the
.process() step's stopped channel sets the RouteStoppingException; the
filter's stopped channel logs the WARN and marks interrupted=true; the
outer pipeline channel skips. The WARN count is asserted only after
future.get() confirms the exchange has fully completed — no races.

RouteStopCutOffCallerRouteTest: address nits from davsclaus review.

- Fix javadoc accuracy (nit 3): the .process() step's own stopped
  channel handler re-enters run() and sets the RouteStoppingException
  (not mock:afterBlock's handler). Route A takes the RedeliveryTask
  isDone() path (not runInterrupted()), since it has a DLC configured.
- Fix release.await() return value (nit 4): check return value and
  fail with a clear message if the 30 s timeout expires.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 15:11 review — all four points are addressed in commit 988bdcd8.

davsclaus findings resolved:

  • ✅ Flaky RouteStopCutOffLogNestedTest (blocking): completely redesigned with the latch pattern, mirroring RouteStopCutOffCallerRouteTest. The old design blocked on direct:shipment (no consumer) and used inflight-browse to detect the exchange; the seda thread could be interrupted before route services were stopped, making the WARN count non-deterministic. New design: route A (seda:start) forwards to route B (direct:nested) via direct (no thread switch). A .process() inside B's filter counts down entered and waits on release. The test waits for entered, stops route B (seda thread is NOT interrupted — direct: has no executor thread of its own), then releases the latch. The processor exits normally; the .process() step's stopped channel sets the RouteStoppingException; the filter's stopped channel logs the WARN and marks interrupted=true; the outer pipeline channel skips. WARN count is asserted only after future.get() confirms the exchange has fully completed — no races.
  • ✅ Javadoc accuracy (nit 3): corrected in both the class-level Javadoc and the inline comments of RouteStopCutOffCallerRouteTest. The description now correctly states that the .process() step's own stopped channel handler re-enters run() (not mock:afterBlock's handler), and that route A takes the RedeliveryTask.isDone() path (not runInterrupted()), since it has a DLC configured.
  • ✅ release.await() return value (nit 4): the unchecked release.await(30, TimeUnit.SECONDS) call is now guarded with if (!release.await(...)) { fail(...); } in both tests — a timed-out wait now fails with a clear message rather than silently continuing.

Optional (WARN count in caller test): not added — the single-WARN guarantee for the .process() cut-off path is already established by RouteStopCutOffLogNestedTest after the redesign. The upgrade guide sentence at :352-357 refers to the nested-EIP scenario covered by that test.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of ef11536884..988bdcd80e. Thanks Guillaume. Only the two tests changed, so my earlier read of MulticastProcessor and RedeliveryErrorHandler stands.

Fixed now:

  • RouteStopCutOffLogNestedTest uses the latch → stopRoute → release shape, and it passed on its first CI attempt.
  • release.await(...) now fails when it times out.
  • The caller-test javadoc now names the .process() channel and route A's isDone() path.

1. The split/multicast single-WARN case is no longer tested, and I think it logs more than once. The rewrite changed the nested test from multicast+split to a filter, so no test counts the WARNs for split/multicast any more, and that is what this PR and the upgrade guide (camel-4x-upgrade-guide-4_24.adoc:354-357) describe. In runNotAllowed() (RedeliveryErrorHandler.java:863-870) the outer stopped handler logs and sets interrupted in one step. But interrupted does not travel up to the parent exchange: ExchangeHelper.copyResults (ExchangeHelper.java:393-404) copies redeliveryExhausted and the exception, not interrupted, and the stoppedOnException branch in MulticastProcessor.doDone (:970-972) copies only the exception. So each split/multicast copy boundary can log its own WARN. By my static trace the caller test logs three: the split item wrapper, the split channel and the multicast step. I traced this, I have not run it.

Could we count the WARNs with ConsumingAppender in the caller test, or add a multicast+split method to the nested test with the same latch shape? Then either carry the flag in doDone when the sub-exchange is interrupted, or narrow the upgrade-guide sentence. These are lines this PR touches, so it is best settled here.

2. Nit: the caller-test javadoc now says the .process() handler logs the WARN (RouteStopCutOffCallerRouteTest.java:41-43, :53-56, :67-69, :101-103, :144-146). That handler only sets the exception (RedeliveryErrorHandler.java:861-862). The outer one logs and marks (:863-870), as the nested test's javadoc (:55-57) already says.

3. Nit: the nested test describes "three nested stopped channels" (:44-48, :76-78, :110-111, :123-124, :144-148). Route B has two, the .process() channel and the filter channel. The third is route A's running channel, which goes through runInterrupted(). The test still catches route A logging a second line, but the !isInterrupted() guard between two stopped handlers is not exercised. Adjust the wording, or nest one more EIP.

CI is green on this head, and the three new tests passed on their first attempt.

Reviewed with Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

…ticastProcessor.doDone(); add multicast+split WARN-count test; fix CallerRouteTest javadoc

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 18:08 review — all three findings are addressed in commit 2ce492f.

davsclaus findings resolved:

  • ✅ Split/multicast single-WARN (finding 1): MulticastProcessor.doDone() now propagates the interrupted flag from the sub-exchange to the original exchange when a stopped nested error handler has set it. This closes the gap davsclaus identified: previously the interrupted flag lived only on the sub-exchange (the split item copy), so the original exchange's channel or a running caller's error handler could log a second WARN. The propagation is added after both the stoppedOnException and copyResults branches — guarded only by isInterrupted() on the sub-exchange, no effect on the normal (non-cut-off) path.
  • ✅ Multicast+split WARN-count test (finding 1): RouteStopCutOffLogNestedTest.multicastSplitCutOffIsOneWarnLine() added using the same latch-based design as the filter test. Route A (seda:start-ms) forwards to route B (direct:nested-ms) which has a split nested inside a multicast. One item. The test asserts exactly one WARN after the exchange completes — validating that the doDone flag propagation prevents a second WARN on the original exchange or a running caller's handler.
  • ✅ CallerRouteTest javadoc (nit 2): all javadoc, method-doc, and inline comments that incorrectly said "the .process() step's own stopped channel handler ... logs the WARN" are corrected. The accurate description: the .process() channel handler sets the RouteStoppingException (exchange had no exception yet); the next outer stopped handler finds the exception already set, logs the WARN, and marks interrupted=true.
  • ✅ Nested test class/method javadoc (nit 3): the class javadoc no longer says "three nested stopped channels" — the filter test's route B has two stopped channels (the .process() step's channel and the filter's channel); route A's channel is running, not stopped. The new multicast+split scenario is documented alongside the filter scenario in the class javadoc.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — all three findings davsclaus raised on 988bdcd80e are addressed in commit 2ce492ff.

What's addressed:

  • ✅ Split/multicast single-WARN: MulticastProcessor.doDone() now propagates interrupted=true from sub-exchange to original, preventing nested stopped error handlers from logging duplicate WARNs across the sub-exchange copy boundary
  • ✅ Upgrade guide single-WARN section: New "Forced route-stop cut-off marks exchange as interrupted in any nested EIP" section accurately documents the setInterrupted(true) behavior and DLC skip consequence
  • ✅ Caller-route test: RouteStopCutOffCallerRouteTest is now a top-level class

New analysis of commit 2ce492ff:

The doDone() propagation logic is correct — ExchangeHelper.copyResults() does not copy the interrupted flag (it's extension state), so the explicit if (subExchange.getExchangeExtension().isInterrupted()) check is necessary and correctly placed. The flag is write-once (true only, never reset), so parallel multicast with multiple recipients cannot undo an earlier propagation.

The runNotAllowed() else if branch is sound: while the branch condition only checks exchange.getException() != null, the inner logCutOffByRouteStop() call guards on instanceof RouteStoppingException before logging or calling setInterrupted(true). A business exception (NPE, etc.) reaching a stopped error handler causes logCutOffByRouteStop() to return false, leaving the exchange untouched. No DLC suppression for genuine failures.

One minor comment-accuracy nit below.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

…ndler fires

When a split() is nested inside a multicast(), and the route is stopped
while an exchange is inflight, two WARN messages were logged instead of one.

Root cause: after the split's error handler marks the sub-exchange
(.0003) as interrupted and logs WARN #1, the split's taskCallback
returns control to the multicast's error handler for the outer
sub-exchange (.0002). At that point .0002 has no exception yet, so
the multicast's error handler sets a new RouteStoppingException on
.0002 and then logs WARN #2 (interrupted=false on .0002).

The existing fix in doDone() that propagates interrupted from
subExchange to original only fires after the taskCallback returns,
i.e. after the outer error handler's callback has already been
scheduled. The outer error handler then runs with interrupted=false.

Fix: in the taskCallback of MulticastReactiveTask, propagate the
interrupted flag from the sub-exchange to the original exchange
immediately, before exchangeResult.accept() is called. This ensures
that when the outer error handler's runNotAllowed() executes, it sees
interrupted=true on the original exchange and skips logging.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on davsclaus's Oct 25 18:08 review — all three findings are addressed in the two new commits (88f9508b, 5740eb32).

davsclaus findings resolved:

  • ✅ Split/multicast double-WARN (finding 1, blocking): 5740eb32 adds interrupted-flag propagation in MulticastReactiveTask.taskCallback — immediately, before exchangeResult.accept() fires. This is the correct insertion point: doDone() propagates only after all sub-exchanges complete and aggregate, which is too late for the outer error handler's runNotAllowed(). The taskCallback path covers the reactive happy path; doDone() covers the stopOnException early-return path. Together they close the gap.
  • ✅ Comment inaccuracy (finding 2, nit): 88f9508b replaces "already has a RouteStoppingException" with "already has an exception" and explicitly notes that logCutOffByRouteStop guards on instanceof RouteStoppingException and is a no-op for other exceptions. Accurate now.
  • ✅ Nested test channel-count wording (finding 3, nit): Was already corrected in 2ce492ff — the class-level javadoc now says "two stopped error-handler channels in route B … plus route A's running channel."

New analysis of 5740eb32:

The fix is structurally sound. The propagation block:

if (exchange.getExchangeExtension().isInterrupted()) {
    original.getExchangeExtension().setInterrupted(true);
}
exchangeResult.accept(exchange);

fires in the same thread that runs the reactive callback, so no race between the write and the outer handler's read. The flag is effectively write-once (true only, never reset), so concurrent multicast recipients setting it redundantly is harmless. The existing doDone() propagation remains correct for the stopOnException early-return branch (:970-972).

RouteStopCutOffLogNestedTest.multicastSplitCutOffIsOneWarnLine directly validates the fix: a single-item split inside a multicast, latch-stopped, asserts exactly one WARN.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants